Spring Security | Note-2

Spring Security Note-2


Spring MVC开发RESTful API

在这一篇笔记中,将开发REST风格的API服务接口,并且在后面的笔记中的认证和授权中,对这些API进行安全保护;

并且会有学习到,如何拦截服务接口来提供一些统一的功能;

如何通过多线程提高服务的性能;

如何自动生成API文档;

伪造服务;


RESTful简介

传统API VS RESTful API
NAME API METHOD RESTful API METHOD
查询 /user/query?name=tom GET /user?name=tom GET
详情 /user/getInfo?id=1 GET /user/1 GET
创建 /user/create?name=tom POST /user POST
修改 /user/update?id=1&name=tom POST /user/1 PUT
删除 /user/delete?id=1 GET /user/1 DELETE
RESTful API
1.用URL描述资源,而不是描述行为;

2.使用HTTP方法描述行为,使用HTTP状态码来表示不同的结果;

3.使用JSON交互数据;

4.RESTful只是一种风格,不是强制的标准;
REST成熟度模型

Level0:使用HTTP作为传输方式;

Level1:引入资源概念;每个资源都有对应的URL;

Level2(RESTful):使用HTTP方法进行不同的操作;使用HTTP状态码来表示不同的结果;

Level3:使用超媒体;在资源的表达中包含了链接信息;


用户查询请求

编写针对RESTful API的测试用例

首先引入针对Spring Boot的测试框架;

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {
// 伪造一个MVC环境
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
public void whenQuerySuccess() throws Exception {
// perform 执行请求,根据请求的结果判断返回的内容是否符合期望
// MockMvcRequestBuilders.get() 模拟发出get请求
// contentType发出内容的请求是UTF-8 JSON格式
// andExpect(),编写返回的期望
// 期望返回的状态是MockMvcResultMatchers.status().isOk() 即200
mockMvc.perform(MockMvcRequestBuilders.get("/user").contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
}
}
使用注解声明RESTful API

@RestController 标明这个Controller提供REST API

@RequestMapping 及其变体(映射HTTP请求URL到JAVA方法)

@RequestParam 映射请求参数到JAVA方法的参数

@PageableDefault 指定分页参数默认值

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class UserController {
@RequestMapping(value = "/user",method = RequestMethod.GET)
public List<User> query(){
List<User> users = new ArrayList<>();
users.add(new User());
users.add(new User());
users.add(new User());
return users;
}
}
在RESTful API中传递参数

此时我们在请求中,加入

1
2
public List<User> query(@RequestParam String username){}
@RequestParam(required = false,name = "username",defaultValue = "rex")

required :指明参数是否必传;

name :指明参数在URL传递的名称;

defaultValue:当参数为空时的默认值;

那么在测试用例中,我们对于的需要加上参数,不然将报出400的错误;

1
.param("username","rex")

将查询的条件,封装成一个对象,直接传入查询的对象;

1
2
3
4
5
6
public class UserQueryCondition {
private String username;
private int age;
private int ageTo;
private String xxx;
}
1
2
3
public List<User> query(UserQueryCondition condition, @PageableDefault(size = 17,page = 2,sort = "username,asc") Pageable pageable){
System.out.println(ReflectionToStringBuilder.toString(condition, ToStringStyle.MULTI_LINE_STYLE));
}
1
2
3
4
5
6
7
.param("username","rex")
.param("age","18")
.param("ageTo","60")
.param("xxx","yyy")
.param("size","15")
.param("page","3")
.param("sort","age,desc")

用户详情请求

1
2
3
4
5
6
7
@Test
public void whenGetInfoSuccess() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.username").value("tom"));
}
@PathVaribale 映射URL片段到JAVA方法的参数
1
2
3
4
5
6
@RequestMapping(value = "/user/{id}",method = RequestMethod.GET)
public User getInfo(@PathVariable String id){
User user = new User();
user.setUsername("tom");
return user;
}
在URL声明中使用正则表达式

希望传入的id必须是数字,使用正则表达式规范传入的参数;

1
@RequestMapping(value = "/user/{id:\\d+}",method = RequestMethod.GET)
@JsonView控制JSON输出方式

场景:假设在query()中,不希望返回用户的password,但是在getinfo()中可返回用户的password,在这两个服务中,返回不同的字段;

使用步骤
1.使用接口来声明多个视图

2.在值对象的get方法上指定视图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class User {
public interface UserSimpleView{};
public interface UserDetailView extends UserSimpleView{};

private String username;
private String password;

@JsonView(UserSimpleView.class)
public String getUsername() {
return username;
}

@JsonView(UserDetailView.class)
public String getPassword() {
return password;
}
}
3.在Controller方法上指定视图
1
2
3
4
5
@RequestMapping(value = "/user",method = RequestMethod.GET)
@JsonView(User.UserSimpleView.class)

@RequestMapping(value = "/user/{id:\\d+}",method = RequestMethod.GET)
@JsonView(User.UserDetailView.class)
重构
1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping(value = "/user",method = RequestMethod.GET)
--> @GetMapping("/user")

@RequestMapping(value = "/user/{id:\\d+}",method = RequestMethod.GET)
--> @GetMapping("/user/{id:\\d+}")

@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping
@GetMapping("/{id:\\d+}")
}

用户创建请求

1
2
3
4
5
6
7
8
9
@Test
public void whenCreateSuccess() throws Exception {
String content = "{\"username\":\"tom\",\"password\":null}";
mockMvc.perform(MockMvcRequestBuilders.post("/user")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1));
}
@RequestBody 映射请求体到 JAVA方法的参数
1
2
3
4
5
@PostMapping
public User create(@RequestBody User user){
user.setId("1");
return user;
}
日期类型参数的处理

为了解决每一个服务端所使用的时间展示方法不一样,有一些使用年月日,有一些使用时分秒;

后台采取时间戳的方式,传递时间数据;

最终的展示格式,由前端(服务端)进行自我定义;

@Valid注解和BindingResult验证请求参数的合法性并处理校验结果
1
2
@NotBlank
private String password;
1
2
3
4
@PostMapping
public User create(@Valid @RequestBody User user){
...
}

为了对请求既能返回错误,又能对请求做出响应,则需要用上BindingResult;

带着错误信息进入到方法体中,继续执行;

1
2
3
4
5
6
7
8
@PostMapping
public User create(@Valid @RequestBody User user, BindingResult errors){
if(errors.hasErrors()){
errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage()));
}
user.setId("1");
return user;
}

控制台返回:may not be empty


用户修改请求

常用的验证注解
注解 解释
@NotNull 值不能为空
@Null 值必须为空
@Pattern(regex=) 字符串必须匹配正则表达式
@Size(min=,max=) 集合的元素数量在范围内
@CreditCardNumber(ignoreNonDigitCharacters=) 字符串必须是信用卡号
@Email 字符串必须是Email地址
@Length(min=,max=) 检查字符串的长度
@NotBlank 字符串必须有字符
@NotEmpty 字符串不为NULL,集合有元素
@Range(min=,max=) 数字必须在范围内
@SafeHtml 字符串是安全的HTML
@URL 字符串是合法的URL
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void whenUpdateSuccess() throws Exception {
Date date = new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
System.out.println(date.getTime());
String content = "{\"id\":\"1\", \"username\":\"tom\",\"password\":null,\"birthday\":" + date.getTime() + "}";
String result = mockMvc.perform(MockMvcRequestBuilders.put("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"))
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
1
2
3
4
5
6
7
8
9
10
11
12
@PutMapping("/{id:\\d+}")
public User update(@Valid @RequestBody User user, BindingResult errors){
if(errors.hasErrors()){
errors.getAllErrors().stream().forEach(error -> {
FieldError fieldError = (FieldError)error;
String message = fieldError.getField() +" "+ error.getDefaultMessage();
System.out.println(message);
});
}
user.setId("1");
return user;
}

控制台返回:

password may not be empty
birthday must be in the past

自定义错误消息
1
2
3
4
5
@NotBlank(message = "密码不能为空")
private String password;

@Past(message = "生日必须是过去的时间")
private Date birthday;
1
2
3
4
5
if(errors.hasErrors()){
errors.getAllErrors().stream().forEach(error -> {
System.out.println(error.getDefaultMessage());
});
}

控制台返回:

密码不能为空
生日必须是过去的时间

自定义校验注解
1
2
3
4
5
6
7
8
9
10
// 注解可标注在方法和字段上
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
// 执行注解时,检验的类
@Constraint(validatedBy = MyConstraintValidator.class)
public @interface MyConstraint {
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyConstraintValidator implements ConstraintValidator<MyConstraint,Object>{
@Autowired
private HelloService helloService;
@Override
public void initialize(MyConstraint myConstraint) {
System.out.println("My Constraint Validator Init");
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
helloService.greeting("tom");
System.out.println(value);
return false;
}
}
1
2
@MyConstraint(message = "测试")
private String username;

控制台返回:

My Constraint Validator Init
greeting
tom
测试


用户删除请求

1
2
3
4
5
6
@Test
public void whenDeleteSuccess() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.delete("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk());
}
1
2
3
4
@DeleteMapping("/{id:\\d+}")
public void delete(@PathVariable String id){
System.out.println(id);
}